#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <sys/wait.h>
#include "http_server.h"


typedef struct   sockaddr sockaddr;
typedef struct   sockaddr_in sockaddr_in ;



//一次从socket中读取以行数据
//把数据放在buf缓冲区中
//如果读取失败，返回值就是-1；
//遇到\n或\r\n或\r证明读取完成
int ReadLine(int  sock,char  buf[],ssize_t  size){
	//1.从socket中一次读取一个字符
	char  c='\0';
	ssize_t   i=0;//当前读了多少个字符
	//结束条件；
	//a)读到的长度太长，达到了缓冲区的上限
	//b)读到了结束标志，并把所有结束标字都转换成\n

	while(i<size-1&&c!='\n'){
		ssize_t  read_size=recv(sock,&c,1,0);
		//读取失败有两种
		if(read_size<0){
			return -1;
		}
		if(read_size==0){
			//因为预期是要读到\n这样的换行符，
			//结果还没有读得到就先读了EOF，这种情况我们也暂时认为是失败的
			return -1;
		}
		if(c=='\r')
		{
			//当前遇到了\r,但还需要确定下一个字符是不是\n
			//MSG_PEEK选项从内核的缓冲区中读取数据，
			//但是读到的数据不会从缓冲区中删除掉。
			recv(sock,&c,1,MSG_PEEK);
			if (c=='\n'){
				//此时的分隔符就是\r\n
				recv(sock,&c,1,0);
			}else 
			{
				//当前分隔符确定是\r，此时把分隔符转换成\n
				c='\n';
			}
		}
		//只要上面的c读到的是\r,那么if结束后，c都变成了\n
		//这种方式就是把前面的\r和\r\n两种情况都统一成了\n
		buf[i++]=c;

	}
	buf [i]='\0';
	//  printf ("ReadLine\n");
	return i;//真正想缓冲区中放置的字符的个数
}


int Split(char  input[], const char *split_char,char* output[],int output_size)
{
	//使用strtok
	int i=0;
	char *tmp=NULL;//保存上次的切分结果
	char * pch;
	//使用线程安全的strtok_r代替strtok
	//这一点是以后非常容易出错的一点
	pch = strtok_r (input,split_char,&tmp);
	while (pch != NULL)
	{
		if (i>=output_size)
		{
			return i;
		}
		output[i++]=pch;
		pch = strtok_r (NULL, split_char,&tmp);
	}

	//printf ("Split\n");
	return i;

}

int ParseFirstLine(char  first_line[],char**p_url,char  **p_method)
{
	//把首行按空格进行字符串切分
	//切分得到的每一个部分，就放在tok数组里
	//返回值，就是tok数组包含几个元素。

	char  *tok[10];
	//最后一个参数10表示tok数组中最多可以放几个元素
	int tok_size=Split(first_line," ",tok,10);
	if (tok_size!=3)
	{
		printf ("Split  failed!   tok_size =%d\n",tok_size);
		return -1;
	}
	*p_method=tok[0];
	*p_url=tok[1];

	// printf ("ParseFirstLine\n");
	return 0;
}



int  ParseQueryString (char  *url,char  **p_url_path,char **p_query_string )
{
	*p_url_path=url;
	char *p=url;
	for (;*p!='\0';++p)
	{
		if (*p=='?')
		{
			*p='\0';
			*p_query_string =p+1;
			return 0;
		}
	}
	//循环结束都没有找到?,说明这个请求不带query_string
	*p_query_string=NULL;
	// printf ("ParseQueryString\n");
	return 0;
}



int    ParseHeader (int sock ,int *content_length)
{
	//1.循环从socket中读取一行。
	//2.判断当前行是不是Content—Length
	//3.如果是Content-Length就直接把value读出来
	//4.如果不是就直接丢弃
	//5.读到空行，循环结束。
	char buf [SIZE]={0};
	while (1)
	{
		//1.循环从socket中读取一行。
		ssize_t  read_size=ReadLine (sock,buf,sizeof (buf));
		//处理读失败的情况
		if (read_size<=0)
		{
			return -1;
		}
		//处理读完的情况
		if (strcmp (buf ,"\n")==0)
		{
			return 0;
		}
		//2.判定当前行是不是Content-Length
		//如果是Content-Length就直接把value读出来
		//如果不是就直接丢弃
		const  char *content_length_str="Content-Length: ";
		if (content_length!=NULL&&strncmp(buf,content_length_str,strlen(content_length_str))==0)
		{
			*content_length=atoi(buf+strlen(content_length_str));

		}


	}
	return 0;
	//读到空行循环结束
}



void Handler404(int sock)
{
	//构造一个完整的HTTP响应
	//状态码是404
	//body部分也应该是一个404相关的错误页面。
	const char*  first_line="HTTP/1.1 404 Not Found\n";
	const char*  type_line="Content-Type: text/html;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
	const char *  blank_line="\n";
	const char * html="<head><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"></head>"
		"<h1>您的页面被喵星人吃掉了！！！</h1>";//提示浏览器按照utf8方式解码
	send (sock,first_line ,strlen(first_line),0);
	send (sock,type_line ,strlen(type_line),0);
	send (sock,blank_line ,strlen(blank_line),0);
	send (sock,html ,strlen(html),0);
	return;
}



void PrintRequest (Request*req)
{
	printf ("method: %s\n",req->method);
	printf ("url_path: %s\n",req->url_path);
	printf ("query_string: %s\n",req->query_string);
	printf ("content_length: %d\n",req->content_length);
	return;
}



int IsDir(const char *file_path)
{
	struct stat st;
	int ret=stat(file_path,&st);
	if (ret<0)
	{
		return 0;
	}
	if (S_ISDIR(st.st_mode))
	{
		return 1;
	}
	return 0;
}


void HandlerFilePath(const  char*url_path,char  file_path[])
{
	//(a,给url_path 加上前缀名（HTTP服务器的根目录）
	//url_path-->/index.html
	//file_path--->./wwwroot/index.html
	sprintf (file_path,"./wwwroot%s",url_path);
	//b)例如url_path是/，此时url_path 其实就是一个目录。
	//如果是目录的话，就演给这个目录之中追加一个index.html 
	//url_path /或者 /image/
	if (file_path[strlen(file_path)-1]=='/')
	{
		strcat(file_path,"index.html");
	}
	//url_path=>/image
	if (IsDir(file_path))
	{
		strcat (file_path,"/index.html");
	}
	return ;

}



ssize_t  GetFileSize(const char*  file_path)
{
	struct stat  st;
	int ret=stat(file_path,&st);
	if (ret<0)
	{
		//打开文件失败，很可能是文件不存在
		//此时直接返回文件长度为0
		return  0;
	}
	return st.st_size;
}




int WriteStaticFile (int sock,const char*  file_path)
{
	//1.打开文件
	int fd=open (file_path,O_RDONLY);
	if (fd<0)
	{
		//文件描述符不够用
		//文件不存在（404）不好意思找不到
		perror ("open");
		return 404;
	}
	//2.把构造出来的HTTP响应写到socket之中
	//1）.写入首行
	//2）.写入head
	//3）.写入空行
	//4）.写入body(文件内容)
	const char *  first_line="HTTP/1.1 200 OK\n";
	send (sock,first_line,strlen(first_line ),0);
	//const char*  type_line="Content-Type: text/html;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
	//const char*  type_line="Content-Type: image/png;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
	//send (sock,type_line ,strlen(type_line),0);
	//两个都不添，让浏览器自己决定
	const char *  blank_line="\n";
	send (sock,blank_line ,strlen(blank_line),0);
	/*ssize_t  file_size=GetFileSize(file_path);
	  ssize_t  i=0;
	  for(;i<file_size;i++)
	  {
	  char c;
	  read(fd,&c,1);
	  send (sock,&c,1,0);
	  }
	  */
	sendfile(sock,fd,NULL,GetFileSize(file_path));
	//3.关闭文件
	close (fd);
	return 200;
}



int HandlerStaticFile (int sock ,Request *req)
{
	//1根据url_path获取到文件在服务器上的真实路径。
	char  file_path[SIZE]={0};
	HandlerFilePath(req->url_path,file_path);
	//2。读取文件，把文件的内容直接写道socket之中
	int err_code=WriteStaticFile (sock,file_path);
	return err_code;
}

int    HandlerCGIFather(int new_sock,int   father_read,int father_write,int  child_pid,Request  *req){
	//1如果是POST请求，就把body写到管道中
	if (strcasecmp(req->method,"POST")==0){
		int i=0;
		char  c='\0';
		for (;i<req->content_length;++i){
			read(new_sock,&c,1);
			write(father_write,&c,1);
		}
	}
	//2构造HTTP响应
	const char *  first_line="HTTP/1.1 200 OK\n";
	send (new_sock,first_line,strlen(first_line ),0);
	const char*  type_line="Content-Type: text/html;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
	//const char*  type_line="Content-Type: image/png;charset=utf-8\n";//提示浏览器按照utf-8方式解码于下面成双重保证
	send (new_sock,type_line ,strlen(type_line),0);
	//两个都不添，让浏览器自己决定
	const char *  blank_line="\n";
	send (new_sock,blank_line ,strlen(blank_line),0);
	//循环的从管道中读取数据并写入数据到socket
	char  c='\0';
	while (read(father_read,&c,1)>0){
		send(new_sock,&c,1,0);
	}
	//4回收子进程的资源
	waitpid(child_pid,NULL,0);
	return 200;
}


int   HandlerCGIChild(int child_read,int child_write,Request*  req){
	//1.设置必要的环境变量
	char   method_env[SIZE]={0};
	sprintf (method_env,"REQUEST_METHOD=%s",req->method);
	putenv(method_env);
	//还需要设置QUERY_STRING或者是CONTENT_LENGTH
	if(strcasecmp(req->method,"GET")==0){
		char  query_string_env[SIZE]={0};
		sprintf (query_string_env,"QUERY_STRING=%s",req->query_string);

	}else {
		char content_length_env[SIZE]={0};
		sprintf (content_length_env,"CONTENT_LENGTH=%d",req->content_length);

	}
	//2把标准输入输出重定向到管道里
	dup2(child_read,0);
	dup2(child_write,1);
	//3对子进程进行程序替换
	//  url_path: /cgi-bin/test
	//  file_path: ./wwwroot/cgi-bin/test
	char   file_path[SIZE]={0};
	HandlerFilePath(req->url_path,file_path);
        
	execl(file_path,file_path,NULL);
        exit(1);//当程序替换失败时。一定让子进程终止，如果子进程不终止，因为父子进程是同一块代码,父进程一直在Listen状态一直等待,子进程也会等待,所以要直接结束进程，避免一直等待端口数据的返回；
//有  exec   l    lp  le   v   vp   ve,首先看知不知道可执行文件的完整路径 ，如有就可以不带P，因为P是在PATH中找，再看要不要环境变量，如果不用，就不需要带e,这里因为通过putenv ()的方式直接设到了ENV里面所以不用，说一直接用execl()
	return 200;
}



int HandlerCGI(int  new_sock,Request  *req)
{
	int err_code=200;
	//1.创建一对匿名管道
	int fd1[2],fd2[2];
	int ret =pipe(fd1);
	if (ret<0)
	{
		return 404;
	} 
	ret=pipe (fd2);
	if (ret<0)
	{
		close(fd1[0]);
		close(fd1[1]);
		return 404;
	}
	//fd1,fd2这种变量名的描述性太差，后面直接用的话
	//是非常容易弄混的，所以直接在此处定义几个
	//更加明确的变量名来描述该文件描述符的用途
	int   father_read=fd1[0];
	int   child_write=fd1[1];
	int   father_write=fd2[1];
	int   child_read=fd2[0];
	//2.创建子进程
	ret=fork();
	//3.父子进程各执行不同的逻辑
	if (ret>0){

		//father
		//此处父进程优先关闭这两个管道的文件描述符
		//是为了后续父进程从子进程这里读数据时，能够读到EOF，对于管道来说，所有写端关闭，继续读，才有EOF，而此时所有写端，一方面是父进程需要关闭，另一方面子进程也需要关闭。所以此处父进程先关闭不必要的写端之后，后续子进程用完了
		//直接关闭，父进程也就读到了EOF
		close (child_read);
		close(child_write);
		err_code=HandlerCGIFather(new_sock,father_read,father_write,ret,req);
	}else if (ret==0){

		//child
		close (father_read);
		close (father_write);
		err_code=HandlerCGIChild(child_read,child_write,req);
	}else{
		perror("fork");
		close (fd1[0]);
		close (fd1[1]);
		close (fd2[0]);
		close (fd2[1]);
		return  404;
	}
	//4.收尾工作和错误处理
	return 200;
}



void  HandlerRequest(int new_sock)
{
	int err_code=200;//错误码初始200，默认没错
	//1.读取并解析请求（反序列化）
	Request req;
	memset (&req,0,sizeof (req));
	//a)从socket中读取出首行
	if (ReadLine(new_sock,req.first_line,sizeof (req.first_line))<0)
	{
		//失败处理
		err_code =404;
		goto END;
	}
	//b)解析首行，从首行中解析出url和method
	if (ParseFirstLine(req.first_line,&req.url,&req.method))
	{
		//失败处理
		err_code =404;
		goto END;

	}
	//c)解析url，从url中解析出url_path和query_string
	if (ParseQueryString(req.url,&req.url_path,&req.query_string))
	{
		//失败处理
		err_code =404;
		goto END;

	}
	//d)解析Header，丢弃了大部分header,只读取Content—Length
	if (ParseHeader(new_sock,&req.content_length))
	{
		//失败处理
		err_code =404;
		goto END;

	}


	PrintRequest (&req);
	//2.静态/动态方式生成页面
	//3.把生成结果写回客户端上
	if (strcasecmp(req.method,"GET")==0&&req.query_string==NULL)
	{
		//a)如果是GET方式请求，并且没有query_string,
		//那么返回静态页面
		err_code=HandlerStaticFile(new_sock,&req);
	}else  if (strcasecmp(req.method,"GET")==0&&req.query_string!=NULL)
	{
		//b)如果是GET方式请求，并且有query_string,
		//那么返回动态页面
		err_code=HandlerCGI(new_sock,&req);
	}else  if (strcasecmp(req.method,"POST")==0)
	{
		//c)如果请求是POST类型的（一定是带参的，参数是通过body来传给服务器的），那么也返回动态页面
		err_code=HandlerCGI(new_sock,&req);
	}else  
	{
		//失败处理
		err_code =404;
		goto END;
	}

	//错误处理；直接返回一个404的HTTP响应
END:
	if (err_code !=200)
	{
		Handler404(new_sock);
	}
	close(new_sock);
	return;

}




void *ThreadEntry(void*arg)//	因为在64位机中指针占8个字节，如果强转回int会有丢失数据的危险
{
	int new_sock=(int)arg;
	//使用HandlerRequest完成具体的处理请求过程
	//相当于线程入口函数只有一个包装，真正干活的是这个函数，这样最大的好处还是解耦和
	//一旦需要把服务器改成多进程或者IO多路复用的形式
	//代码的改动都是比较小的
	HandlerRequest(new_sock);
	return NULL;
}




//服务器启动
void HttpServerStart(const  char*  ip,short   port)
{
	int listen_sock=socket(AF_INET,SOCK_STREAM,0);
	if (listen_sock<0)
	{
		perror("socket");
		return ;
	}
	int opt=1;
	setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt));//加上这个函数是为了使端口处于TIME-WAIT时复用地址
	sockaddr_in addr;
	addr.sin_family=AF_INET;
	addr.sin_addr.s_addr=inet_addr(ip);
	addr.sin_port=htons(port);

	int ret=bind(listen_sock,(sockaddr*)&addr,sizeof (addr));
	if (ret<0)
	{
		perror("bind");
		return ;
	}
	ret=listen(listen_sock,5);
	if (ret<0)
	{
		perror("listen");
		return ;
	}
	printf ("ServerInit  OK\n");
	while (1)
	{
		sockaddr_in peer;
		socklen_t  len=sizeof (peer);
		int   new_sock=accept(listen_sock,(sockaddr*)&peer,&len);
		if (new_sock<0)
		{
			perror("accept");
			continue;
		}
		//使用多线程的方式来实现TCP服务器
		pthread_t  tid;
		//       printf ("ThreadEntry\n");
		pthread_create(&tid,NULL,ThreadEntry,(void *)new_sock);
		pthread_detach(tid);
	}

}




//主函数的参数   ./http_server  [ip]   [port]
int main (int argc,char*   argv[])
{
	if (argc!=3)
	{
		printf ("Usage   ./http_server  [ip]   [port]\n");
		return 1;
	}
	HttpServerStart( argv[1],atoi(argv[2]));
	return 0;
}
